package view

import (
	"io"
	"os"
	"fmt"
	"sync"
	"bytes"
	"strings"
	"io/ioutil"
	"html/template"
	"path/filepath"
)

var funcMap = template.FuncMap{
	"layout": 	func() (string, error) {return "", fmt.Errorf("no layout defined")},
	"include": 	func() (string, error) {return "", nil},
}

type View struct {
	dir 		string
	ext 		string
	left    	string
	right   	string
	layout  	string
	options		[]string
	mute    	sync.RWMutex
	handle		map[string]interface{}
	Template  	*template.Template
	asset   	func(name string) ([]byte, error)
	names   	func() []string
}

func NewView(dir string) *View {
	return &View{
		dir: 	dir,
		ext: 	".html",
		left: 	"{{",
		right:	"}}",
		layout: "",
		handle: make(map[string]interface{}),
		asset: nil,
		names: nil,
	}
}

func (v *View) Ext(ext string) *View {
	v.ext = ext
	return v
}

func (v *View) Layout(layout string) *View {
	v.layout = layout
	return v
}

func (v *View) Option(options ...string) *View {
	v.mute.Lock()
	v.options = append(v.options, options...)
	v.mute.Unlock()
	return v
}

func (v *View) Delims(left string, right string) *View {
	v.left, v.right = left, right
	return v
}

func (v *View) AddFunc(name string, value interface{}) *View {
	v.mute.Lock()
	v.handle[name] = value
	v.mute.Unlock()
	return v
}

func (v *View) BinData(asset func(name string) ([]byte, error), names func() []string) *View {
	v.asset, v.names = asset, names
	return v
}

func (v *View) Load() error {
	if v.asset != nil && v.names != nil {
		return v.binData()
	}

	dir, err := filepath.Abs(v.dir)
	if err != nil {
		return err
	}

	v.dir = dir

	return v.templateLoad()
}

func (v *View) Render(w io.Writer, name string, data interface{}) error {
	v.handleFunc(name, data)

	if len(v.layout) > 0 {
		name = v.templateName(v.layout)
	} else {
		name = v.templateName(name)
	}

	return v.Template.ExecuteTemplate(w, name, data)
}

func (v *View) executeTemplate(name string, data interface{}) (*bytes.Buffer, error) {
	name = v.templateName(name)
	b := new(bytes.Buffer)
	return b, v.Template.ExecuteTemplate(b, name, data)
}

func (v *View) handleFunc(name string, data interface{}) {
	name = v.templateName(name)

	fn := template.FuncMap{
		"layout": func() (template.HTML, error) {
			b, err := v.executeTemplate(name, data)
			return template.HTML(b.String()), err
		},
		"include": func(name string) (template.HTML, error) {
			b, err := v.executeTemplate(name, data)
			return template.HTML(b.String()), err
		},
	}

	for k, v := range v.handle {
		fn[k] = v
	}

	if t := v.Template.Lookup(name); t != nil {
		t.Funcs(fn)
	}
}

func (v *View) templateName(name string) string {
	ext := filepath.Ext(name)
	if len(ext) <= 0 {
		name = fmt.Sprintf("%s%s", name, v.ext)
	} else if ext != v.ext {
		name = strings.TrimSuffix(name, filepath.Ext(name))
		name = fmt.Sprintf("%s%s", name, v.ext)
	}
	return name
}

func (v *View) templateInit(path string, data []byte) (err error) {
	if v.Template == nil {
		v.Template = template.New(v.dir)
		v.Template.Delims(v.left, v.right)
	}

	body := string(data)
	name := filepath.ToSlash(path)
	temp := v.Template.New(name)

	_, err = temp.Option(v.options...).Funcs(funcMap).Funcs(v.handle).Parse(body)
	if err != nil {
		return err
	}
	return nil
}

func (v *View) templateLoad() error {
	return filepath.Walk(v.dir, func(path string, fn os.FileInfo, err error) error {
		if fn == nil || fn.IsDir() {
			return nil
		}

		re, err := filepath.Rel(v.dir, path)
		if err != nil {
			return err
		}

		ext := filepath.Ext(path)
		if ext != v.ext {
			return nil
		}

		data, err := ioutil.ReadFile(path)
		if err != nil {
			return err
		}

		return v.templateInit(re, data)
	})
}

func (v *View) binData() error {
	if len(v.dir) > 0 {
		if v.dir[0] == '.' {
			v.dir = v.dir[1:]
		}
		if v.dir[0] == '/' || v.dir[0] == os.PathSeparator {
			v.dir = v.dir[1:]
		}
	}

	for _, path := range v.names() {
		if !strings.HasPrefix(path, v.dir) {
			continue
		}

		if filepath.Ext(path) != v.ext {
			continue
		}

		re, err := filepath.Rel(v.dir, path)
		if err != nil {
			return err
		}

		data, err := v.asset(path)
		if err != nil {
			return err
		}

		err = v.templateInit(re, data)
		if err != nil {
			return err
		}
	}

	return nil
}
